/* -*- Mode: js; js-indent-level: 2; -*- */
/*
 * Copyright 2011 Mozilla Foundation and contributors
 * Licensed under the New BSD license. See LICENSE or:
 * http://opensource.org/licenses/BSD-3-Clause
 */

/**
 * This is a helper function for getting values from parameter/options
 * objects.
 *
 * @param args The object we are extracting values from
 * @param name The name of the property we are getting.
 * @param defaultValue An optional value to return if the property is missing
 * from the object. If this is not specified and the property is missing, an
 * error will be thrown.
 */
function getArg(aArgs, aName, aDefaultValue) {
  if (aName in aArgs) {
    return aArgs[aName]
  } else if (arguments.length === 3) {
    return aDefaultValue
  }
  throw new Error('"' + aName + '" is a required argument.')
}
exports.getArg = getArg

const urlRegexp =
  /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/
const dataUrlRegexp = /^data:.+\,.+$/

function urlParse(aUrl) {
  const match = aUrl.match(urlRegexp)
  if (!match) {
    return null
  }
  return {
    scheme: match[1],
    auth: match[2],
    host: match[3],
    port: match[4],
    path: match[5],
  }
}
exports.urlParse = urlParse

function urlGenerate(aParsedUrl) {
  let url = ''
  if (aParsedUrl.scheme) {
    url += aParsedUrl.scheme + ':'
  }
  url += '//'
  if (aParsedUrl.auth) {
    url += aParsedUrl.auth + '@'
  }
  if (aParsedUrl.host) {
    url += aParsedUrl.host
  }
  if (aParsedUrl.port) {
    url += ':' + aParsedUrl.port
  }
  if (aParsedUrl.path) {
    url += aParsedUrl.path
  }
  return url
}
exports.urlGenerate = urlGenerate

const MAX_CACHED_INPUTS = 32

/**
 * Takes some function `f(input) -> result` and returns a memoized version of
 * `f`.
 *
 * We keep at most `MAX_CACHED_INPUTS` memoized results of `f` alive. The
 * memoization is a dumb-simple, linear least-recently-used cache.
 */
function lruMemoize(f) {
  const cache = []

  return function (input) {
    for (let i = 0; i < cache.length; i++) {
      if (cache[i].input === input) {
        const temp = cache[0]
        cache[0] = cache[i]
        cache[i] = temp
        return cache[0].result
      }
    }

    const result = f(input)

    cache.unshift({
      input,
      result,
    })

    if (cache.length > MAX_CACHED_INPUTS) {
      cache.pop()
    }

    return result
  }
}

/**
 * Normalizes a path, or the path portion of a URL:
 *
 * - Replaces consecutive slashes with one slash.
 * - Removes unnecessary '.' parts.
 * - Removes unnecessary '<dir>/..' parts.
 *
 * Based on code in the Node.js 'path' core module.
 *
 * @param aPath The path or url to normalize.
 */
const normalize = lruMemoize(function normalize(aPath) {
  let path = aPath
  const url = urlParse(aPath)
  if (url) {
    if (!url.path) {
      return aPath
    }
    path = url.path
  }
  const isAbsolute = exports.isAbsolute(path)

  // Split the path into parts between `/` characters. This is much faster than
  // using `.split(/\/+/g)`.
  const parts = []
  let start = 0
  let i = 0
  while (true) {
    start = i
    i = path.indexOf('/', start)
    if (i === -1) {
      parts.push(path.slice(start))
      break
    } else {
      parts.push(path.slice(start, i))
      while (i < path.length && path[i] === '/') {
        i++
      }
    }
  }

  let up = 0
  for (i = parts.length - 1; i >= 0; i--) {
    const part = parts[i]
    if (part === '.') {
      parts.splice(i, 1)
    } else if (part === '..') {
      up++
    } else if (up > 0) {
      if (part === '') {
        // The first part is blank if the path is absolute. Trying to go
        // above the root is a no-op. Therefore we can remove all '..' parts
        // directly after the root.
        parts.splice(i + 1, up)
        up = 0
      } else {
        parts.splice(i, 2)
        up--
      }
    }
  }
  path = parts.join('/')

  if (path === '') {
    path = isAbsolute ? '/' : '.'
  }

  if (url) {
    url.path = path
    return urlGenerate(url)
  }
  return path
})
exports.normalize = normalize

/**
 * Joins two paths/URLs.
 *
 * @param aRoot The root path or URL.
 * @param aPath The path or URL to be joined with the root.
 *
 * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a
 *   scheme-relative URL: Then the scheme of aRoot, if any, is prepended
 *   first.
 * - Otherwise aPath is a path. If aRoot is a URL, then its path portion
 *   is updated with the result and aRoot is returned. Otherwise the result
 *   is returned.
 *   - If aPath is absolute, the result is aPath.
 *   - Otherwise the two paths are joined with a slash.
 * - Joining for example 'http://' and 'www.example.com' is also supported.
 */
function join(aRoot, aPath) {
  if (aRoot === '') {
    aRoot = '.'
  }
  if (aPath === '') {
    aPath = '.'
  }
  const aPathUrl = urlParse(aPath)
  const aRootUrl = urlParse(aRoot)
  if (aRootUrl) {
    aRoot = aRootUrl.path || '/'
  }

  // `join(foo, '//www.example.org')`
  if (aPathUrl && !aPathUrl.scheme) {
    if (aRootUrl) {
      aPathUrl.scheme = aRootUrl.scheme
    }
    return urlGenerate(aPathUrl)
  }

  if (aPathUrl || aPath.match(dataUrlRegexp)) {
    return aPath
  }

  // `join('http://', 'www.example.com')`
  if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {
    aRootUrl.host = aPath
    return urlGenerate(aRootUrl)
  }

  const joined =
    aPath.charAt(0) === '/'
      ? aPath
      : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath)

  if (aRootUrl) {
    aRootUrl.path = joined
    return urlGenerate(aRootUrl)
  }
  return joined
}
exports.join = join

exports.isAbsolute = function (aPath) {
  return aPath.charAt(0) === '/' || urlRegexp.test(aPath)
}

/**
 * Make a path relative to a URL or another path.
 *
 * @param aRoot The root path or URL.
 * @param aPath The path or URL to be made relative to aRoot.
 */
function relative(aRoot, aPath) {
  if (aRoot === '') {
    aRoot = '.'
  }

  aRoot = aRoot.replace(/\/$/, '')

  // It is possible for the path to be above the root. In this case, simply
  // checking whether the root is a prefix of the path won't work. Instead, we
  // need to remove components from the root one by one, until either we find
  // a prefix that fits, or we run out of components to remove.
  let level = 0
  while (aPath.indexOf(aRoot + '/') !== 0) {
    const index = aRoot.lastIndexOf('/')
    if (index < 0) {
      return aPath
    }

    // If the only part of the root that is left is the scheme (i.e. http://,
    // file:///, etc.), one or more slashes (/), or simply nothing at all, we
    // have exhausted all components, so the path is not relative to the root.
    aRoot = aRoot.slice(0, index)
    if (aRoot.match(/^([^\/]+:\/)?\/*$/)) {
      return aPath
    }

    ++level
  }

  // Make sure we add a "../" for each component we removed from the root.
  return Array(level + 1).join('../') + aPath.substr(aRoot.length + 1)
}
exports.relative = relative

const supportsNullProto = (function () {
  const obj = Object.create(null)
  return !('__proto__' in obj)
})()

function identity(s) {
  return s
}

/**
 * Because behavior goes wacky when you set `__proto__` on objects, we
 * have to prefix all the strings in our set with an arbitrary character.
 *
 * See https://github.com/mozilla/source-map/pull/31 and
 * https://github.com/mozilla/source-map/issues/30
 *
 * @param String aStr
 */
function toSetString(aStr) {
  if (isProtoString(aStr)) {
    return '$' + aStr
  }

  return aStr
}
exports.toSetString = supportsNullProto ? identity : toSetString

function fromSetString(aStr) {
  if (isProtoString(aStr)) {
    return aStr.slice(1)
  }

  return aStr
}
exports.fromSetString = supportsNullProto ? identity : fromSetString

function isProtoString(s) {
  if (!s) {
    return false
  }

  const length = s.length

  if (length < 9 /* "__proto__".length */) {
    return false
  }

  /* eslint-disable no-multi-spaces */
  if (
    s.charCodeAt(length - 1) !== 95 /* '_' */ ||
    s.charCodeAt(length - 2) !== 95 /* '_' */ ||
    s.charCodeAt(length - 3) !== 111 /* 'o' */ ||
    s.charCodeAt(length - 4) !== 116 /* 't' */ ||
    s.charCodeAt(length - 5) !== 111 /* 'o' */ ||
    s.charCodeAt(length - 6) !== 114 /* 'r' */ ||
    s.charCodeAt(length - 7) !== 112 /* 'p' */ ||
    s.charCodeAt(length - 8) !== 95 /* '_' */ ||
    s.charCodeAt(length - 9) !== 95 /* '_' */
  ) {
    return false
  }
  /* eslint-enable no-multi-spaces */

  for (let i = length - 10; i >= 0; i--) {
    if (s.charCodeAt(i) !== 36 /* '$' */) {
      return false
    }
  }

  return true
}

/**
 * Comparator between two mappings where the original positions are compared.
 *
 * Optionally pass in `true` as `onlyCompareGenerated` to consider two
 * mappings with the same original source/line/column, but different generated
 * line and column the same. Useful when searching for a mapping with a
 * stubbed out mapping.
 */
function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {
  let cmp = strcmp(mappingA.source, mappingB.source)
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.originalLine - mappingB.originalLine
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.originalColumn - mappingB.originalColumn
  if (cmp !== 0 || onlyCompareOriginal) {
    return cmp
  }

  cmp = mappingA.generatedColumn - mappingB.generatedColumn
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.generatedLine - mappingB.generatedLine
  if (cmp !== 0) {
    return cmp
  }

  return strcmp(mappingA.name, mappingB.name)
}
exports.compareByOriginalPositions = compareByOriginalPositions

/**
 * Comparator between two mappings with deflated source and name indices where
 * the generated positions are compared.
 *
 * Optionally pass in `true` as `onlyCompareGenerated` to consider two
 * mappings with the same generated line and column, but different
 * source/name/original line and column the same. Useful when searching for a
 * mapping with a stubbed out mapping.
 */
function compareByGeneratedPositionsDeflated(
  mappingA,
  mappingB,
  onlyCompareGenerated
) {
  let cmp = mappingA.generatedLine - mappingB.generatedLine
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.generatedColumn - mappingB.generatedColumn
  if (cmp !== 0 || onlyCompareGenerated) {
    return cmp
  }

  cmp = strcmp(mappingA.source, mappingB.source)
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.originalLine - mappingB.originalLine
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.originalColumn - mappingB.originalColumn
  if (cmp !== 0) {
    return cmp
  }

  return strcmp(mappingA.name, mappingB.name)
}
exports.compareByGeneratedPositionsDeflated =
  compareByGeneratedPositionsDeflated

function strcmp(aStr1, aStr2) {
  if (aStr1 === aStr2) {
    return 0
  }

  if (aStr1 === null) {
    return 1 // aStr2 !== null
  }

  if (aStr2 === null) {
    return -1 // aStr1 !== null
  }

  if (aStr1 > aStr2) {
    return 1
  }

  return -1
}

/**
 * Comparator between two mappings with inflated source and name strings where
 * the generated positions are compared.
 */
function compareByGeneratedPositionsInflated(mappingA, mappingB) {
  let cmp = mappingA.generatedLine - mappingB.generatedLine
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.generatedColumn - mappingB.generatedColumn
  if (cmp !== 0) {
    return cmp
  }

  cmp = strcmp(mappingA.source, mappingB.source)
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.originalLine - mappingB.originalLine
  if (cmp !== 0) {
    return cmp
  }

  cmp = mappingA.originalColumn - mappingB.originalColumn
  if (cmp !== 0) {
    return cmp
  }

  return strcmp(mappingA.name, mappingB.name)
}
exports.compareByGeneratedPositionsInflated =
  compareByGeneratedPositionsInflated

/**
 * Strip any JSON XSSI avoidance prefix from the string (as documented
 * in the source maps specification), and then parse the string as
 * JSON.
 */
function parseSourceMapInput(str) {
  return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, ''))
}
exports.parseSourceMapInput = parseSourceMapInput

/**
 * Compute the URL of a source given the the source root, the source's
 * URL, and the source map's URL.
 */
function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) {
  sourceURL = sourceURL || ''

  if (sourceRoot) {
    // This follows what Chrome does.
    if (sourceRoot[sourceRoot.length - 1] !== '/' && sourceURL[0] !== '/') {
      sourceRoot += '/'
    }
    // The spec says:
    //   Line 4: An optional source root, useful for relocating source
    //   files on a server or removing repeated values in the
    //   “sources” entry.  This value is prepended to the individual
    //   entries in the “source” field.
    sourceURL = sourceRoot + sourceURL
  }

  // Historically, SourceMapConsumer did not take the sourceMapURL as
  // a parameter.  This mode is still somewhat supported, which is why
  // this code block is conditional.  However, it's preferable to pass
  // the source map URL to SourceMapConsumer, so that this function
  // can implement the source URL resolution algorithm as outlined in
  // the spec.  This block is basically the equivalent of:
  //    new URL(sourceURL, sourceMapURL).toString()
  // ... except it avoids using URL, which wasn't available in the
  // older releases of node still supported by this library.
  //
  // The spec says:
  //   If the sources are not absolute URLs after prepending of the
  //   “sourceRoot”, the sources are resolved relative to the
  //   SourceMap (like resolving script src in a html document).
  if (sourceMapURL) {
    const parsed = urlParse(sourceMapURL)
    if (!parsed) {
      throw new Error('sourceMapURL could not be parsed')
    }
    if (parsed.path) {
      // Strip the last path component, but keep the "/".
      const index = parsed.path.lastIndexOf('/')
      if (index >= 0) {
        parsed.path = parsed.path.substring(0, index + 1)
      }
    }
    sourceURL = join(urlGenerate(parsed), sourceURL)
  }

  return normalize(sourceURL)
}
exports.computeSourceURL = computeSourceURL
